/*
 * Atomic Predicates for Transformers
 * 
 * Copyright (c) 2015 UNIVERSITY OF TEXAS AUSTIN. All rights reserved. Developed
 * by: HONGKUN YANG and SIMON S. LAM http://www.cs.utexas.edu/users/lam/NRL/
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * with the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimers.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimers in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the UNIVERSITY OF TEXAS AUSTIN nor the names of the
 * developers may be used to endorse or promote products derived from this
 * Software without specific prior written permission.
 * 
 * 4. Any report or paper describing results derived from using any part of this
 * Software must cite the following publication of the developers: Hongkun Yang
 * and Simon S. Lam, Scalable Verification of Networks With Packet Transformers
 * Using Atomic Predicates, IEEE/ACM Transactions on Networking, October 2017,
 * Volume 25, No. 5, pages 2900-2915 (first published as IEEE Early Access
 * Article, July 2017, Digital Object Identifier: 10.1109/TNET.2017.2720172).
 * 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH
 * THE SOFTWARE.
 */
package transformer;

import java.io.*;
import java.util.*;

import common.APComputer;
import common.FWDAPSet;
import common.PositionTuple;

public class NetworkAP extends Network{

    /**
     * 
     */
    private static final long serialVersionUID = -7169850699567111373L;

    APComputer apc;
    double[] ap_times;

    public NetworkAP(String name) throws IOException
    {
        super(name, false, false);
        ap_times = new double[2];
    }

    public double[] get_ap_times()
    {
        return ap_times;
    }

    /**
     * add nat and compute ap
     * @param nat_num
     */
    public void add_nat_ap(int nat_num)
    {
        ArrayList<NATBox> nats = add_nat(nat_num);

        HashSet<Integer> predicates = new HashSet<Integer>();
        for(Box b : boxes.values())
        {
            if(b instanceof Device)
                predicates.addAll(((Device) b).fwbdds.values());
        }
        apc = new APComputer(bddengine);

        long t1 = System.currentTimeMillis();
        int size = apc.addPredicates(new ArrayList<Integer>(predicates));
        long t2 =System.currentTimeMillis();

        double elapse_time = (t2-t1);
        ap_times[0] = elapse_time;

        System.out.println(size);

        t1 = System.currentTimeMillis();
        for(NATBox nat : nats)
        {
            nat.updateTranslatedPredicates(apc);
        }

        ArrayList<Integer> transformedPredicates = new ArrayList<Integer> ();
        for(NATBox nat: nats)
        {
            for(int i : nat.translatedPredicates_in.values())
            {
                transformedPredicates.add(i);
            }
            for(int i : nat.translatedPredicates_out.values())
            {
                transformedPredicates.add(i);
            }
        }

        size = apc.addPredicates(transformedPredicates);
        t2 = System.currentTimeMillis();

        elapse_time = (t2-t1);
        if(nat_num == 0)
        {
            ap_times[1] = 0;
        }else
        {
            ap_times[1] = elapse_time;
        }

        System.out.println(size);
        //long t11 = System.nanoTime();
        apc.computeExpressions();
        //long t22 = System.nanoTime();

        //System.out.println((t22-t11)/1000000.0);

        FWDAPSet.setUniverse(apc.getAllAP());

        for(Box b: boxes.values())
        {
            b.setaps(apc);
        }
    }

    /**
     * pick two numbers randomly from min, max
     * @param min
     * @param max
     * @return
     */
    public static int[] randPicksRange(int min, int max)
    {
        int[] picks = new int[2];
        Random rand = new Random(System.currentTimeMillis());
        HashSet<Integer> S = new HashSet<Integer>();
        
        while(S.size() <2)
        {
            S.add(rand.nextInt((max - min) + 1) + min);
        }
        int cnt = 0;
        for(int i : S)
        {
            picks[cnt] = i;
            cnt ++;
        }
        return picks;
    }

    public void update_port_predicate_nat()
    {
        ArrayList<int[]> updates = new ArrayList<int[]> ();
        ArrayList<NATBox> nats = add_nat(9);

        HashSet<Integer> predicates = new HashSet<Integer>();
        for(Box b : boxes.values())
        {
            int count = 0;
            // for each router, pick two port predicates
            int[] update = new int[2];
            if(b instanceof Device)
            {
                int size = ((Device) b).fwbdds.size();
                int[] picks = randPicksRange(1,size);
                for(int port_predicate : ((Device) b).fwbdds.values())
                {
                    count ++;
                    if(count == picks[0])
                    {
                        update[0] = port_predicate;
                    }else{
                        if(count == picks[1])
                        {
                            update[1] = port_predicate;
                            updates.add(update);
                        }
                        predicates.add(port_predicate);
                    }
                }
            }
        }
        apc = new APComputer(bddengine);

        int size = apc.addPredicates(new ArrayList<Integer>(predicates));
        apc.computeExpressions();


        compute_ap_nat(nats);

        long update_time = 0;

        System.out.println(updates.size() + " updates");

        for(int[] update : updates)
        {
            long t1 = System.currentTimeMillis();

            apc.update_r(update[1]);
            apc.update_a(update[0]);

            compute_ap_nat(nats);
            long t2 = System.currentTimeMillis();


            update_time += t2-t1;

            System.out.println(t2-t1);
        }

        System.out.println("update time: " + (1.0*update_time/updates.size())+ " ms");

    }

    /**
     * assume atomic predicates for filters are computed; expressions are also computed
     * @param nats
     * @return
     */
    public void compute_ap_nat(ArrayList<NATBox> nats)
    {

        if(nats.isEmpty())
        {
            return ;
        }

        for(NATBox nat : nats)
        {
            nat.updateTranslatedPredicates(apc);
        }

        ArrayList<Integer> transformedPredicates = new ArrayList<Integer> ();
        for(NATBox nat: nats)
        {
            transformedPredicates.addAll(nat.translatedPredicates_in.values());
            transformedPredicates.addAll(nat.translatedPredicates_out.values());
        }

        apc.update_a(transformedPredicates);


        /**
         * port predicates update should reset ap set for each box
         * but now we do not do that, this is for test only
         * b/c we intentionally remove a port predicate from  
         */
        /*
        FWDAPSet.setUniverse(apc.getAllAP());

        for(Box b: boxes.values())
        {
            System.out.println(b.name);
            b.setaps(apc);
        }
         */

    }

    public void update_add_nat_ap()
    {
        ArrayList<NATBox> all_nats = add_nat(9);

        ArrayList<NATBox> nats = new ArrayList<NATBox>(all_nats);
        NATBox later_add = nats.get(0);
        nats.remove(0);


        HashSet<Integer> predicates = new HashSet<Integer>();
        for(Box b : boxes.values())
        {
            if(b instanceof Device)
                predicates.addAll(((Device) b).fwbdds.values());
        }
        apc = new APComputer(bddengine);

        long t1 = System.currentTimeMillis();
        int size = apc.addPredicates(new ArrayList<Integer>(predicates));
        long t2 =System.currentTimeMillis();

        double elapse_time = (t2-t1);
        ap_times[0] = elapse_time;

        System.out.println(size);

        t1 = System.currentTimeMillis();
        for(NATBox nat : nats)
        {
            nat.updateTranslatedPredicates(apc);
        }

        ArrayList<Integer> transformedPredicates = new ArrayList<Integer> ();
        for(NATBox nat: nats)
        {
            for(int i : nat.translatedPredicates_in.values())
            {
                transformedPredicates.add(i);
            }
            for(int i : nat.translatedPredicates_out.values())
            {
                transformedPredicates.add(i);
            }
        }

        size = apc.addPredicates(transformedPredicates);
        t2 = System.currentTimeMillis();

        elapse_time = (t2-t1);

        ap_times[1] = elapse_time;


        System.out.println(size);
        //long t11 = System.nanoTime();
        apc.computeExpressions();
        //long t22 = System.nanoTime();

        //System.out.println((t22-t11)/1000000.0);

        FWDAPSet.setUniverse(apc.getAllAP());

        //for(Box b: boxes.values())
        //{
        //    b.setaps(apc);
        //}

        // add a NAT
        t1 = System.currentTimeMillis();
        transformedPredicates = new ArrayList<Integer> ();

        // here we assume that NAT is a simple case, so we do not consider other nats
        later_add.updateTranslatedPredicates(apc);

        for(int i : later_add.translatedPredicates_in.values())
        {
            transformedPredicates.add(i);
        }
        for(int i : later_add.translatedPredicates_out.values())
        {
            transformedPredicates.add(i);
        }
        boolean notchanged = apc.update_a(transformedPredicates);

        // reset atomic predicates set
        FWDAPSet.setUniverse(apc.getAllAP());


        for(Box b: boxes.values())
        {
            b.setaps(apc);
        }

        t2 = System.currentTimeMillis();
        long ap_update_time = t2-t1;
        HashSet<PositionTuple> pts = getallactiveports();

        // recompute tree
        elapse_time = StateTransferAP.compute_trees_anypkt(this, pts, null);

        System.out.println(pts.size() + " trees");

        System.out.println("AP update takes " + ap_update_time + " ms");
        System.out.println("recompute tree takes " + elapse_time + " ms");

    }

    /**
     * compute atomic predicates for filters
     */
    public void ap_first_iter()
    {
        HashSet<Integer> predicates = new HashSet<Integer>();
        for(Box b : boxes.values())
        {
            if(b instanceof Device)
                predicates.addAll(((Device) b).fwbdds.values());
        }
        apc = new APComputer(bddengine);

        long t1 = System.currentTimeMillis();
        int size = apc.addPredicates(new ArrayList<Integer>(predicates));
        long t2 = System.currentTimeMillis();

        ap_times[0] = t2-t1;

        System.out.println("computing atomic predicates for filters takes "+ap_times[0]+" ms");
        System.out.println(size + " atomic predicates");      
    }

    /**
     * call this after adding IP-in-IP tunnels
     */
    public void ap_ipinip()
    {
        ArrayList<IPinIP> tunnels = new ArrayList<IPinIP> ();
        for(Box b : boxes.values())
        {
            Device d = (Device) b;
            if(d.ip_in_ip != null)
            {
                tunnels.add(d.ip_in_ip);
            }
        }

        long t1 =  System.currentTimeMillis();

        ArrayList<Integer> transformedPredicates = new ArrayList<Integer> ();
        for(IPinIP tunnel : tunnels)
        {
            tunnel.updatePredicatesMap(apc);
            transformedPredicates.addAll(tunnel.predicatesMap.values());
        }

        int size = apc.addPredicates(transformedPredicates);
        System.out.println(size + " atomic predicates.");

        transformedPredicates = new ArrayList<Integer> ();
        for(IPinIP tunnel : tunnels)
        {
            tunnel.updatePredicatesMap(apc);
            transformedPredicates.addAll(tunnel.predicatesMap.values());
        }

        size = apc.addPredicates(transformedPredicates);        

        long t2 = System.currentTimeMillis();
        if(transformedPredicates.isEmpty())
        {
            ap_times[1] = 0;
        }else{
            ap_times[1] = t2-t1;
        }
        System.out.println(size + " atomic predicates.");
        System.out.println(ap_times[1]+" ms to compute atomic predicates for IP-in-IP");


        apc.computeExpressions();

        FWDAPSet.setUniverse(apc.getAllAP());

        for(Box b : boxes.values())
        {
            b.setaps(apc);
            ((Device) b).ip_in_ip.setaps(apc);
        }


    }

    public void compute_ap_ipinip(ArrayList<IPinIP> tunnels)
    {
        ArrayList<Integer> transformedPredicates = new ArrayList<Integer> ();
        for(IPinIP tunnel : tunnels)
        {
            tunnel.updatePredicatesMap(apc);
            transformedPredicates.addAll(tunnel.predicatesMap.values());
        }

        apc.update_a(transformedPredicates);

        transformedPredicates = new ArrayList<Integer> ();
        for(IPinIP tunnel : tunnels)
        {
            tunnel.updatePredicatesMap(apc);
            transformedPredicates.addAll(tunnel.predicatesMap.values());
        }

        apc.update_a(transformedPredicates);        

        /**
         * port predicates update should reset ap set for each box
         * but now we do not do that, this is for test only
         * b/c we intentionally remove a port predicate from  
         */
        /*  
        FWDAPSet.setUniverse(apc.getAllAP());

        for(Box b : boxes.values())
        {
            b.setaps(apc);
            ((Device) b).ip_in_ip.setaps(apc);
        }
         */

    }

    public double ap_ipinip_update()
    {
        ArrayList<IPinIP> tunnels = new ArrayList<IPinIP> ();
        for(Box b : boxes.values())
        {
            Device d = (Device) b;
            if(d.ip_in_ip != null)
            {
                tunnels.add(d.ip_in_ip);
            }
        }

        long t1 =  System.currentTimeMillis();

        ArrayList<Integer> transformedPredicates = new ArrayList<Integer> ();

        // here we need to consider all transformers, b/c packet transformed 
        // by newly added transformer may be later transformed by 
        // an existing packet transformer
        for(IPinIP tunnel : tunnels)
        {
            tunnel.updatePredicatesMap(apc);
            transformedPredicates.addAll(tunnel.predicatesMap.values());
        }

        apc.update_a(transformedPredicates);
        //int size = apc.addPredicates(transformedPredicates);
        //System.out.println(size + " atomic predicates.");

        transformedPredicates = new ArrayList<Integer> ();
        for(IPinIP tunnel : tunnels)
        {
            tunnel.updatePredicatesMap(apc);
            transformedPredicates.addAll(tunnel.predicatesMap.values());
        }

        apc.update_a(transformedPredicates);
        //size = apc.addPredicates(transformedPredicates);        


        FWDAPSet.setUniverse(apc.getAllAP());

        for(Box b : boxes.values())
        {
            b.setaps(apc);
            ((Device) b).ip_in_ip.setaps(apc);
        }

        long t2 = System.currentTimeMillis();

        return (t2-t1)/1.0;

    }


    public static void main(String [] args) throws IOException
    {
        NetworkAP net = new NetworkAP("i2");
        net.update_port_predicate_nat();

    }

}
